# --------------------------------------------------------------------------------
#  NoiseTube Web application
#  ProcessMaps - initial version by KK.
#  Copyright (C) 2008-2013 SONY Computer Science Laboratory Paris
#  Portions contributed by Vrije Universiteit Brussel (BrusSense team), 2008-2013
# --------------------------------------------------------------------------------
#  This library is free software; you can redistribute it and/or modify it under
#  the terms of the GNU Lesser General Public License, version 2.1, as published
#  by the Free Software Foundation.
#  
#  This library is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
#  FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
#  details.
#  
#  You should have received a copy of the GNU Lesser General Public License along
#  with this library; if not, write to:
#    Free Software Foundation, Inc.,
#    51 Franklin Street, Fifth Floor,
#    Boston, MA  02110-1301, USA.
#  
#  Full GNU LGPL v2.1 text: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt
#  NoiseTube project source code repository: http://code.google.com/p/noisetube
# --------------------------------------------------------------------------------
#  More information:
#   - NoiseTube project website: http://www.noisetube.net
#   - Sony Computer Science Laboratory Paris: http://csl.sony.fr
#   - VUB BrusSense team: http://www.brussense.be
# --------------------------------------------------------------------------------


  # NOTE: ; we take grid elements of size 0,000359567100716402 x 0,000567685180885114 which is 40m x 40m (our preliminary GPS tests give standard
  #  deviations of <10m from the average long/lat. However, maximum deviations go up to 60m.)
# (use for example http://www.csgnetwork.com/degreelenllavcalc.html to convert lat/long to meters)

# used in this module: see http://www.movable-type.co.uk/scripts/latlong.html
# NOTE2: All these formulae are for calculations on the basis of a spherical earth (ignoring ellipsoidal effects) – which is accurate enough* for most purposes…
# [In fact, the earth is very slightly ellipsoidal; using a spherical model gives errors typically up to 0.3% – see notes for further details].

require 'color'
require 'maps/enumerable.rb'
require 'geo_features'
require 'rgeo'


module ProcessMaps
  # constants defaults
  EARTHS_RADIUS_KMS =  6371;  # earth's mean radius in km
  GRID_SIZE_IN_COORDS = {lng: 0.000359567100716402, lat: 0.000567685180885114}
  MIN_DB = 10
  MAX_DB = 140
  MIN_MEASURES_COUNT = 0

  @geographic_factory = RGeo::Geographic.spherical_factory(:srid => NTSRID)


  # Cleans the tracks, returns an array of [trackid date time db lat lng] values
def self.track(tracks)
  measures = Track.find(id).geomeasures


end

# creates an aggregated map grid. note: no measures with nil geoms please!
# @param [Integer] the track's id
def self.aggregatedmap(ms)

  grid_with_measures_and_stats = []

  unless ms.blank?
    puts "ProcessMaps: generate bounding box... "
    #get the bounding box
    bbox = boundingbox(ms)
    #get its grids
    grid_with_measures = create_measures_grid(GRID_SIZE_IN_COORDS[:lat],GRID_SIZE_IN_COORDS[:lng],bbox,ms) #with default values

    #puts "ProcessMaps: ..finished creating the grids. start distributing measures.."
    #add measurements - slowest operation
    #grid_with_measures = distribute_measures(ms,grid,MIN_MEASURES_COUNT, MIN_DB, MAX_DB)

    puts "ProcessMaps: ..completed distributing measures. calculating statistics.."
    #add stats
    grid_with_measures_and_stats = calculate_statistics(grid_with_measures)
    puts "ProcessMaps: ..completed calculating statistics at " << Time.now.to_s

  end
  grid_with_measures_and_stats
end

# finds the bounding box of a set of measures WITH GEO
def self.boundingbox(measures)

  # problem with rges's projected window for geos. use
  # simple cartesian plane representation
  bbox_factory = RGeo::Geographic.simple_mercator_factory(:srid => 4326)
  cartesian_bbox = RGeo::Cartesian::BoundingBox.new(bbox_factory)

  measures.each { |measure|
    unless measure.geom.nil?

      cartesian_bbox.add(measure.geom)
    end
  } unless measures.nil?

  cartesian_bbox
end

#returns true if geom (geom MUST be a RGeo::Feature::Point) is in boundingbox
def self.in_boundingbox(geom, boundingbox)

  in_box = false

  in_box = boundingbox.contains_point(geom) unless geom.nil?

  in_box
end


# create-grids - takes spacing (in coords) and min/max lat lng values and
# generates a grid of measurements
# @param [Object] x_spacing
# @param [Object] y_spacing
# @param [Object] boundingbox
  def self.create_grids(x_spacing, y_spacing, boundingbox)

    minx,maxx = boundingbox.min_x(), boundingbox.max_x()

    miny,maxy = boundingbox.min_y(), boundingbox.max_y()

    #holds the overall result
    grid =  limits(x_spacing,y_spacing,minx, maxx, miny, maxy)

    grid
  end


  def self.distribute_measures(measures, grid, minpergrid=MIN_MEASURES_COUNT, mindb=MIN_DB, maxdb=MAX_DB)
    #remove measures that have no geometries
    measures.select! { |m| !m.geom.nil? }

    #performance improvement: sort the measures by lats
    measures.sort_by! { |m| m.geom.lat}

    count = 0
    #map over the grid, assigning the measurements fitting the criteria
    populated_grid = grid.delete_if { |elm|   #we dont need the empty grids...
      count += 1
      puts ".. distributing measures into grid no: " << count.to_s  << " at " << Time.now.to_s   if (count % 10 == 0)


      elm_measures =  measures.select{ |measure|
        # (note due to issue on wiki, we compare lats and y vals, longs with x vals)
        measure.loudness <= maxdb and measure.loudness >= mindb and measure.geom.lat >= elm[:xmin] and measure.geom.lat < elm[:xmax] and measure.geom.lng >= elm[:ymin] and measure.geom.lng < elm[:ymax]

      }
      if  !elm_measures.nil? and (elm_measures.size >= minpergrid)
        #may have nils, remove them
        elm_measures.compact!
        elm[:measures] = elm_measures
        next false
      else
        next true
      end
    }

    populated_grid

  end

  # takes measurements, min or max db values, and min number of elms,
  # and distributes them over a grid according to those conditions
  #note: ignores measures without geoms
  # @param [Object] measures
  # @param [Array] grid
  # @param [Object] minpergrid
  # @param [Integer] maxdb
  # @param [Integer] mindb
  def self.distribute_measures_using_db(grid, start_date, end_date=Time.now, minpergrid=MIN_MEASURES_COUNT, mindb=MIN_DB, maxdb=MAX_DB)
    #performance improvement: sort the measures by lats

    #create multipoint
    #multi_point = @geographic_factory.multi_point(measures.map{|m| m.geom})

    count = 0

    new_grid = grid.map{ |elm|
      elm[:measures] = []
      elm
    }

    #map over the grid, assigning themeasurements fitting the criteria
    populated_grid = new_grid.map { |elm|
      count += 1
      puts ".. distributing measures into grid no: " << count.to_s  << " at " << Time.now.to_s   if (count % 10 == 0)

      elm_measures = Measure.scoped
      elm_measures = elm_measures.where(" measures.geom is not null and measures.made_at >= ? and measures.made_at <= ?", start_date, end_date)
    elm_measures = elm_measures.where("(measures.geom && ST_MakeEnvelope(?, ?, ?, ?, ?) )",elm[:ymin],elm[:xmin], elm[:ymax], elm[:xmax], NTSRID )
      elm_measures = elm_measures.where("measures.loudness >= ? and measures.loudness <= ?", mindb, maxdb)

      elm_measures = elm_measures.all #execute

      if  !elm_measures.nil? and (elm_measures.size >= minpergrid)
        #may have nils, remove them
        elm_measures.compact!
        elm[:measures] = elm_measures
      else
        elm[:measures] = []
      end

      elm
    }

    populated_grid

  end


  # calculate the statistics of a grid with measurements, with avg, min, max,
  # variance, stdev, and count of measurements on the grid
  # @param [Object] grid - the grid with distributed measures
  def self.calculate_statistics(grid)

    grid.each{ |elm|
      elm_loudness = elm[:measures].collect{|measure|
         measure.loudness
      }
      elm[:sum] = elm_loudness.sum
      elm[:mean] = elm_loudness.mean
      elm[:variance] = elm_loudness.variance

      elm[:stdev] = nil
      elm[:stdev] = Math.sqrt elm[:variance] unless elm[:variance].nil?

      elm[:count] = elm_loudness.count
      elm[:maxdb] = elm_loudness.max
      elm[:mindb] = elm_loudness.min
    }

    grid
  end

private

# create-grids (recursive) - takes spacing (in coords) and min/max lat lng values and
# generates a grid of measurements
# @param [Object] x_spacing
# @param [Object] y_spacing
# @param [Object] boundingbox
  def self.create_grids_recursive(x_spacing, y_spacing, boundingbox)  #caused stack overflow! do not use

    minx,maxx = boundingbox.min_x(), boundingbox.max_x()

    miny,maxy = boundingbox.min_y(), boundingbox.max_y()
    #todo: test - change to meters
    distance =
        #holds the overall result
        grid = []

    #find the xs
    x_array = limits_recursive(x_spacing, minx, maxx, [])


    #find the ys for each xs, and zip the coordinates
    x_array.each do |xarr|

      y_array = limits_recursive(y_spacing, miny, maxy, [])

      y_array.each { |yarr|

        grid << {xmin: yarr[0], ymin: xarr[0], xmax: yarr[1], ymax: xarr[1]}   #note: interchanged x, y values
      }
    end

    grid
  end
  # helper for recursive fn
  def self.limits_recursive( x_spacing, minx, maxx, accumulator)

    if (minx + x_spacing) > maxx
      if minx < maxx
        accumulator << [minx, maxx]   #append
      end

      #now do with y values

    else
      limits_recursive(x_spacing, (minx + x_spacing), maxx, accumulator)
      accumulator.unshift [minx, minx + x_spacing] #prepend
    end

    accumulator
  end

  # --------------- end recursive methods ----------------------

  # --------------- one method for measures --------------------
  def self.create_measures_grid(x_spacing, y_spacing, boundingbox, ms, mindb=MIN_DB, maxdb=MAX_DB, mincount=MIN_MEASURES_COUNT)

    minx,maxx = boundingbox.min_x(), boundingbox.max_x()

    miny,maxy = boundingbox.min_y(), boundingbox.max_y()

    #holds the overall result
    grid =  limits_with_measures(x_spacing,y_spacing,minx, maxx, miny, maxy, ms, mindb, maxdb, mincount)

    grid


  end

  # creates the grid, given the measurements(ms), min, max gis values, min, max db values
  # intensive operation for larger grids
  def self.limits_with_measures( x_spacing, y_spacing, minx, maxx, miny, maxy, ms, mindb, maxdb, mincount)

    movingy = miny
    movingx = minx
    count = 0
    grid = []
    #remove measures that have no geometries
    ms.select! { |m| !m.geom.nil? }

    if (minx + x_spacing) > maxx
      if minx < maxx
        elm = {xmin: miny, xmax: maxy, ymin: minx, ymax: maxx}
        grid << add_measures_to_grid_element(elm, ms, mindb, maxdb, mincount)  #append
      end
    else
      while (movingy + y_spacing) <= maxy

        while (movingx + x_spacing) <= maxx
          #keep count
          count += 1
          puts "Generating grid no: "<< count.to_s << " at " << Time.now.to_s   if (count % 10 == 0)
          elm = {xmin: movingy, xmax: (movingy + y_spacing), ymin: movingx, ymax: (movingx + x_spacing)}
          #add the measures to grid here
          temp =  add_measures_to_grid_element(elm, ms, mindb, maxdb, mincount)
          grid <<  temp if !temp.blank?
          puts ".. finished distributing measures into grid at " << Time.now.to_s   if (count % 10 == 0)

          # add x space
          movingx += x_spacing

          #add the last grid - may be less than grid size (note!)
          if (movingx + x_spacing) > maxx
            elm =  {xmin: movingy, xmax: (movingy + y_spacing), ymin: movingx, ymax: maxx}
            temp = add_measures_to_grid_element(elm, ms, mindb, maxdb, mincount)
            grid <<  temp if !temp.blank? and movingx < maxx
          end
        end

        #increase y value
        movingy += y_spacing

        #add the last grid - may be less than grid size (note!)
        if (movingy + y_spacing) > maxy
          if movingy < maxy
            elm = {xmin: movingy, xmax: maxy, ymin: movingx, ymax: maxx}
            temp =  add_measures_to_grid_element(elm, ms, mindb, maxdb, mincount)
            grid << temp if !temp.blank?
          end
        end

        #reset x value (reached end of y)
        movingx = minx
      end
    end

    grid
  end

  #adds measurements to a grid element
  def self.add_measures_to_grid_element(elm, measures, mindb, maxdb, minpergrid)
    elm_measures = []
    measures.delete_if{ |measure|
        # (note due to issue on wiki, we compare lats and y vals, longs with x vals)
        if measure.loudness <= maxdb and measure.loudness >= mindb and measure.geom.lat >= elm[:xmin] and measure.geom.lat < elm[:xmax] and measure.geom.lng >= elm[:ymin] and measure.geom.lng < elm[:ymax]
          elm_measures << measure
          true
        else
          false
        end
      }
      if !elm_measures.nil? and (elm_measures.size >= minpergrid)
        #may have nils, remove them
        elm_measures.compact!

        #remove the measures from original array, less size


        elm[:measures] = elm_measures
      else
        elm[:measures] = []
      end
     elm
  end

  # --------------- end method for distributing --------------------
  def self.limits( x_spacing, y_spacing, minx, maxx, miny, maxy)

    movingy = miny
    movingx = minx
    count = 0
    grid = []
    if (minx + x_spacing) > maxx
      if minx < maxx
        grid << {xmin: miny, xmax: maxy, ymin: minx, ymax: maxx}   #append
      end
    else
      while (movingy + y_spacing) <= maxy

        while (movingx + x_spacing) <= maxx
          #keep count
          count += 1
          puts "Generating grid row no: "<< count.to_s << " at " << Time.now.to_s   if (count % 10 == 0)

          grid << {xmin: movingy, xmax: (movingy + y_spacing), ymin: movingx, ymax: (movingx + x_spacing)}
          movingx += x_spacing

          #add the last grid - may be less than grid size (note!)
          if (movingx + x_spacing) > maxx
            grid << {xmin: movingy, xmax: (movingy + y_spacing), ymin: movingx, ymax: maxx} if movingx < maxx
          end
        end

        #increase y value
        movingy += y_spacing

        #add the last grid - may be less than grid size (note!)
        if (movingy + y_spacing) > maxy
          if movingy < maxy
            grid << {xmin: movingy, xmax: maxy, ymin: movingx, ymax: maxx}
          end
        end

        #reset x value
        movingx = minx
      end
    end

    grid
  end

  #sets the grid according to the coordinates of the bbox
  def self.project_grid(bbox, grid)

    grid.each {  |elm|
      elm[:xmax] = bbox.x_min() + elm[:xmax]
      elm[:ymax] = bbox.y_min() + elm[:ymax]
      elm[:xmin] = bbox.x_min() + elm[:xmin]
      elm[:ymin] = bbox.y_min() + elm[:ymin]
    }

    grid
  end

  #* Returns the distance from this point to the supplied point, in km
  #* (using Haversine formula)
  #* from: Haversine formula - R. W. Sinnott, "Virtues of the Haversine",
  #                            Sky and Telescope, vol 68, no 2, 1984
  #  also see http://stackoverflow.com/questions/365826/calculate-distance-between-2-gps-coordinates
  def self.distance_in_meters(lat1, lng1, lat2, lng2)

    lat1,lat2 = to_rad(lat1), to_rad(lat2) #convert to Rads
    lng1,lng2 = to_rad(lng1), to_rad(lng2)

    lat_distance = (lat2 - lat1)
    lng_distance = (lng2 - lng1)

    a = Math.sin(lat_distance/2 )**2 + ( Math.sin(lng_distance/2)**2 * Math.cos(lat1) * Math.cos(lat2) )

    c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))

    d =  EARTHS_RADIUS_KMS * c

    d * 1000  #convert to meters
  end

  #convert to rad
  def self.to_rad(deg)
    deg * 0.017453292519943295 #Math::PI/180
  end

  # distance_in_coords: calculate the coordinate delta for a given distance
  # @param [Object] meters
  # @param [Object] point
  # @return [Integer] the coordinates in degrees
  #TODO - for now use GRID_SIZE constant
  def self.distance_in_coords(meters, point)

  end

end